use moss_core::error::Error;
use moss_core::util::type_of_option;
use proc_macro2::TokenStream;
use quote::format_ident;
use quote::quote;
use syn::Data;
use syn::DeriveInput;
use syn::FieldsNamed;
use syn::Result;

use super::from_meta::FieldAttr;

pub fn named_fields_asign_values_token(
    variant_token: &TokenStream,
    fields: &FieldsNamed,
) -> Result<TokenStream> {
    let mut define_idents_token = quote!();
    let mut asign_values_token = quote!();
    let mut match_field_token = quote!();
    for field in &fields.named {
        let option_ty = type_of_option(&field.ty);
        let mut field_ty = &field.ty;
        let mut is_option_ty = false;
        if let Some(option_ty) = option_ty {
            field_ty = option_ty;
            is_option_ty = true;
        }
        let moss_attr = FieldAttr::try_from(&field.attrs)?;
        let path_name = moss_attr.get_name(field.ident.as_ref())?;
        let field_ident = &field.ident;
        let field_ident_str = field_ident
            .as_ref()
            .map(|t| t.to_string())
            .unwrap_or_default();
        let define_ident = format_ident!("_{field_ident_str}");

        define_idents_token = quote! {
            #define_idents_token
            let mut #define_ident: Option<#field_ty> = None;
        };
        match_field_token = quote! {
            #match_field_token
            if path.is_ident(#path_name) {
                #define_ident = Some(<#field_ty as moss::from_meta::NamedFieldValueParser>::parse(&meta)?);
                matched = true;
                continue;
            }
        };
        if is_option_ty {
            asign_values_token = quote! {
                #asign_values_token
                #field_ident: #define_ident,
            };
        } else {
            asign_values_token = quote! {
                #asign_values_token
                #field_ident: #define_ident.ok_or(moss::error::Error::missing_field(#field_ident_str))?,
            };
        }
    }

    let generated: TokenStream = quote! {
        #define_idents_token
        let mut matched = false;
        loop {
            if stream.is_empty() {
                break;
            }
            if matched {
                let _: syn::Token![,] = stream.parse()?;
                if stream.is_empty() {
                    break;
                }
            }
            let meta: syn::Meta = stream.parse()?;
            let path = meta.path();
            #match_field_token

            let unknow_name = <syn::Path as quote::ToTokens>::to_token_stream(path).to_string();
            return Err(moss::error::Error::unknown_field(unknow_name.as_str()).into());
        }
        let result = Self #variant_token {
            #asign_values_token
        };
        Ok(result)
    };
    Ok(generated)
}

pub fn expand(input: &DeriveInput) -> Result<TokenStream> {
    let for_ident = &input.ident;
    let generated;
    match &input.data {
        Data::Struct(data) => match &data.fields {
            syn::Fields::Named(fields_named) => {
                generated = named_fields_asign_values_token(&quote!(), fields_named)?;
            }
            syn::Fields::Unnamed(_) => return Err(Error::unexpected_type("UnnamedStruct").into()),
            syn::Fields::Unit => return Err(Error::unexpected_type("UnitStruct").into()),
        },
        Data::Enum(_) => return Err(Error::unexpected_type("Enum").into()),
        Data::Union(_) => return Err(Error::unexpected_type("Union").into()),
    }

    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();

    let generated = quote! {
        impl #impl_generics moss::from_meta::UnnamedFieldParser for #for_ident #ty_generics #where_clause {

            fn parse(stream: syn::parse::ParseStream) -> syn::Result<Self> {
                stream.parse()
            }
        }

        impl #impl_generics syn::parse::Parse for #for_ident #ty_generics #where_clause {
            fn parse(stream: syn::parse::ParseStream) -> syn::Result<Self> {
                #generated
            }
        }
    };
    Ok(generated)
}
